/*
 * Copyright (C) 2003, Velocityme Partnership
 * NodeSessionBean.java
 *
 * Created on 16 May 2003, 07:22
 */

package com.velocityme.session;

import com.velocityme.entity.ChangeDeltaItemTypeBean;
import com.velocityme.entity.ChangeDeltaTypeBean;
import com.velocityme.entity.PermissionBean;
import javax.ejb.*;
import javax.naming.*;
import java.util.*;
import com.velocityme.interfaces.*;
import com.velocityme.utility.ChangeDeltaHelper;
import com.velocityme.utility.DurationFormat;
import com.velocityme.utility.DurationRemaining;
import com.velocityme.utility.InvalidKeyException;
import com.velocityme.utility.PermissionDeniedException;
import com.velocityme.utility.SearchEntry;
import com.velocityme.utility.TimeEntry;
import com.velocityme.utility.TimeSummaryTable;
import com.velocityme.valueobjects.*;
import java.io.ByteArrayOutputStream;
import java.rmi.RemoteException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 *
 * @author  Robert Crida Work
 * @ejb.bean
 *           type="Stateless"
 *           cmp-version="2.x"
 *           name="NodeSession"
 *           jndi-name="ejb/NodeSession"
 *           view-type="local"
 *           transaction-type="Container"
 * @ejb.transaction type="Required"
 *
 * @ejb.util generate="physical"
 */
public class NodeSessionBean implements SessionBean {
    
    private SessionContext m_context;
    
    private ChangeDeltaHelper m_cdHelper;
    
    private SearchSessionLocal m_searchSessionLocal;
    
    private PermissionLocal m_permissionNodeCreate;
    private PermissionLocal m_permissionNodeEdit;
    private PermissionLocal m_permissionNodeDelete;
    private PermissionLocal m_permissionNodePostComment;
    private PermissionLocal m_permissionNodeView;
    private ChangeDeltaTypeLocal m_changeDeltaTypeCreate;
    
    /**
     * If the user has permission to create a new Node in the specified parent
     * node then go ahead and create it and log an appropriate ChangeDelta.
     *
     * @param p_key User's security key
     * @param p_parentNode The node which will contain the new Node
     * @param p_nodeValue The value details for the Node
     *
     * @ejb.interface-method view-type="local"
     **/
    public NodeLocal createNode(com.velocityme.interfaces.KeySessionLocal p_keyLocal,
                                com.velocityme.interfaces.NodeLocal p_parentNodeLocal,
                                com.velocityme.valueobjects.NodeValue p_nodeValue,
                                java.lang.String p_finalType)
        throws PermissionDeniedException, InvalidKeyException {
        try {
            if (m_searchSessionLocal.getAssignedPermissionsLocal(p_keyLocal, p_parentNodeLocal).contains(m_permissionNodeCreate)) {
		p_nodeValue.setType("Node");
                p_nodeValue.setFinalType(p_finalType);
                DBObjectLocal dBObjectLocal = DBObjectUtil.getLocalHome().create();
                NodeLocal nodeLocal = NodeUtil.getLocalHome().create(dBObjectLocal, p_nodeValue, p_parentNodeLocal);
                ChangeDeltaLocal changeDeltaLocal = ChangeDeltaUtil.getLocalHome().create(new Date(), ChangeDeltaTypeBean.CREATE, p_keyLocal.getUserLocal(), dBObjectLocal);
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.NAME, p_nodeValue.getName());
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.DESCRIPTION, p_nodeValue.getDescription());
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.PARENT_NODE, p_parentNodeLocal.getPathName());
                return nodeLocal;
            }
            else
                throw new PermissionDeniedException();
        }
        catch(NamingException e) {
            throw new EJBException(e);
        }
        catch(CreateException e) {
            throw new EJBException(e);
        }
    }
    
    /**
     * If the user has permission to edit the Node then do so and log the
     * changes with an appropriate ChangeDelta.
     *
     * @param p_key User's security key
     * @param p_node The Node object being edited
     * @param p_nodeValue The value details for the Node
     *
     * @ejb.interface-method view-type="local"
     **/
    public void editNode(com.velocityme.interfaces.KeySessionLocal p_keyLocal,
                         com.velocityme.interfaces.NodeLocal p_nodeLocal,
                         com.velocityme.valueobjects.NodeValue p_nodeValue)
        throws PermissionDeniedException, InvalidKeyException {
        try {
            if (m_searchSessionLocal.getAssignedPermissionsLocal(p_keyLocal, p_nodeLocal).contains(m_permissionNodeEdit)) {
                ChangeDeltaLocal changeDeltaLocal = ChangeDeltaUtil.getLocalHome().create(new Date(), ChangeDeltaTypeBean.EDIT, p_keyLocal.getUserLocal(), p_nodeLocal.getDBObjectLocal());
                if (!p_nodeValue.getName().equals(p_nodeLocal.getName()))
                    changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.NAME, p_nodeValue.getName());
                if (!p_nodeValue.getDescription().equals(p_nodeLocal.getDescription()))
                    changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.DESCRIPTION, p_nodeValue.getDescription());
                p_nodeLocal.setNodeValue(p_nodeValue);
            }
            else
                throw new PermissionDeniedException();
        }
        catch(NamingException e) {
            throw new EJBException(e);
        }
        catch(CreateException e) {
            throw new EJBException(e);
        }
    }
    
    /**
     * If the user has permission to access a node then return the local object.
     *
     * @param p_key User's security key
     * @param p_nodePK The Node object to lookup
     *
     * @ejb.interface-method view-type="local"
     **/
    public NodeLocal getNodeLocal(KeySessionLocal p_keyLocal, 
                                  NodePK p_nodePK)
        throws PermissionDeniedException, InvalidKeyException {
        try {
            NodeLocal nodeLocal = NodeUtil.getLocalHome().findByPrimaryKey(p_nodePK);
            if (m_searchSessionLocal.getAssignedPermissionsLocal(p_keyLocal, nodeLocal).contains(m_permissionNodeView)) {
                return nodeLocal;
            }
            else
                throw new PermissionDeniedException();
        }
        catch(NamingException e) {
            throw new EJBException(e);
        }
        catch(FinderException e) {
            throw new EJBException(e);
        }
    }
    
    /**
     * If the user has permission to post a comment in the current Node then
     * put the message into the change log
     *
     * @param p_key User's security key
     * @param p_nodeLocal The Node object being edited
     * @param p_message The message for the log
     *
     * @ejb.interface-method view-type="local"
     **/
    public void postComment(KeySessionLocal p_keyLocal,
                            NodeLocal p_nodeLocal,
                            String p_message,
                            java.io.File p_file,
                            java.lang.String p_contentType,
                            java.io.InputStream p_stream)
        throws PermissionDeniedException, InvalidKeyException {
        try {
            if (m_searchSessionLocal.getAssignedPermissionsLocal(p_keyLocal, p_nodeLocal).contains(m_permissionNodePostComment)) {
                ChangeDeltaLocal changeDeltaLocal = ChangeDeltaUtil.getLocalHome().create(new Date(), ChangeDeltaTypeBean.POST_COMMENT, p_keyLocal.getUserLocal(), p_nodeLocal.getDBObjectLocal());
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.MESSAGE, p_message);
                FileAttachmentSessionUtil.getLocalHome().create().createFileAttachment(changeDeltaLocal, p_file, p_contentType, p_stream);
                if (p_nodeLocal.getType().equals("Task")) {
                    TaskLocal taskLocal = p_nodeLocal.getTaskLocal();
                    NotificationMechanismSessionUtil.getLocalHome().create().sendNotificationOfInterest(p_keyLocal, taskLocal, new HashSet(taskLocal.getInterestedContactablesLocal()));
                }
            }
            else
                throw new PermissionDeniedException();
        }
        catch(NamingException e) {
            throw new EJBException(e);
        }
        catch(CreateException e) {
            throw new EJBException(e);
        }
            
    }
    
    /**
     * Get an XML string describing the report generated for the given person and
     * node and date range.
     *
     * @param p_key User's security key
     * @param p_nodeLocal Node that the times are required for
     * @param p_personLocal The person whose times are required
     * @param p_recursive Should the sub-tree be searched?
     * @param p_start Start date for time
     * @param p_stop Stop date for time
     *
     * @ejb.interface-method view-type="local"
     **/
    public java.lang.String searchTimeLogXML(com.velocityme.interfaces.KeySessionLocal p_keyLocal,
                                             com.velocityme.interfaces.NodeLocal p_nodeLocal,
                                             com.velocityme.interfaces.ContactableLocal[] p_contactablesLocal,
                                             boolean p_recursive,
                                             boolean p_taskSummary,
                                             boolean p_typeSummary,
                                             boolean p_stateSummary,
                                             boolean p_entries,
                                             Date p_start,
                                             Date p_stop) {
        try {
            // set up the heading lists
            List personSets = new ArrayList();
            List setNames = new ArrayList();
            Set allPersons = new HashSet();
            for (int i = 0; i < p_contactablesLocal.length; i++) {
                setNames.add(p_contactablesLocal[i].getNodeLocal().getName());
                Set setContactablePersons = ContactableSessionUtil.getLocalHome().create().getNestedPersonSet(p_keyLocal, p_contactablesLocal[i]);
                personSets.add(setContactablePersons);
                allPersons.addAll(setContactablePersons);
            }
            // only add the total if there is more than one column of results
            if (setNames.size() > 1) {
                setNames.add("Total");
                personSets.add(allPersons);
            }
            String xmlString = new String();
            String nodePathName = p_nodeLocal.getPathName();
            Pattern pattern = Pattern.compile(nodePathName);
            DocumentBuilder parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            Document document = parser.newDocument();
            document.appendChild(document.createProcessingInstruction("xml-stylesheet", "type='text/xsl' href='timeLog.xsl'"));
            document.appendChild(document.createElement("TimeLog"));
            Element rootElement = document.getDocumentElement();
            Element entryElement = document.createElement("Entries");
            DateFormat dateTimeFormatter = new SimpleDateFormat("yyyy/MM/dd HH:mm");
            DateFormat dateFormatter = DateFormat.getDateInstance();
            rootElement.setAttribute("date", dateFormatter.format(new Date()));
            SearchLocal searchLocal = m_searchSessionLocal.createSearchSpaceLocal(p_keyLocal, p_nodeLocal, new Boolean(p_recursive));
            // create a search containing all the person nodes
            Collection personNodesLocal = new ArrayList();
            Iterator i = allPersons.iterator();
            while(i.hasNext()) {
                personNodesLocal.add(((PersonLocal)i.next()).getContactableLocal().getNodeLocal());
            }
            SearchLocal searchPersonLocal = SearchUtil.getLocalHome().create();
            SearchLevelLocal searchLevelPersonLocal = SearchLevelUtil.getLocalHome().create(searchPersonLocal, new Integer(0), personNodesLocal);
            Collection timesLocal = TimeUtil.getLocalHome().findTimeForPersons(searchLocal, searchPersonLocal, p_start, p_stop);
            m_searchSessionLocal.removeSearchSpaceLocal(p_keyLocal, searchLocal);
            m_searchSessionLocal.removeSearchSpaceLocal(p_keyLocal, searchPersonLocal);
            Iterator te = timesLocal.iterator();
            DurationAccummulator personAccummulator = new DurationAccummulator();
            DurationAccummulator taskAccummulator = new DurationAccummulator();
            DurationAccummulator taskTypeAccummulator = new DurationAccummulator();
            DurationAccummulator stateAccummulator = new DurationAccummulator();
            while(te.hasNext()) {
                TimeLocal teLocal = (TimeLocal)te.next();
                Long duration = teLocal.getDuration();
                PersonLocal personLocal = teLocal.getPersonLocal();
                Matcher matcher = pattern.matcher(teLocal.getTaskLocal().getNodeLocal().getPathName());
                String truncPathName = matcher.replaceFirst(".");
                // update the individual sums as required
                personAccummulator.add("Total", personLocal, duration);
                if (p_taskSummary == true)
                    taskAccummulator.add(truncPathName, personLocal, duration);
                if (p_typeSummary == true)
                    taskTypeAccummulator.add(new TaskStateMachineValueToString(teLocal.getTaskLocal().getTaskStateMachineLocal().getTaskStateMachineValue()), personLocal, duration);
                if (p_stateSummary == true)
                    stateAccummulator.add(new StateValueToString(teLocal.getStateLocal().getStateValue()), personLocal, duration);
                // create the entries as required
                if (p_entries == true) {
                    Element teElement = document.createElement("Entry");
                    teElement.setAttribute("date", dateTimeFormatter.format(teLocal.getStartTime()));
                    teElement.setAttribute("duration", DurationFormat.formatDuration(teLocal.getDuration().longValue()));
                    teElement.setAttribute("person", teLocal.getPersonLocal().getContactableLocal().getNodeLocal().getName());
                    teElement.setAttribute("task", truncPathName);
                    teElement.setAttribute("state", teLocal.getStateLocal().getNodeLocal().getName());
                    teElement.appendChild(document.createTextNode(teLocal.getDescription()));
                    entryElement.appendChild(teElement);
                }
            }
            Element summaryElement = document.createElement("Summaries");
            summaryElement.setAttribute("node", nodePathName);
            summaryElement.setAttribute("start", dateFormatter.format(p_start));
            summaryElement.setAttribute("stop", dateFormatter.format(p_stop));
            rootElement.appendChild(summaryElement);
            if (p_entries == true)
                rootElement.appendChild(entryElement);
            // append person summary
            personAccummulator.addSummaryTable(document, summaryElement, "Person", setNames, personSets);
            // append task summary
            if (p_taskSummary == true)
                taskAccummulator.addSummaryTable(document, summaryElement, "Task", setNames, personSets);
            // append task type summary
            if (p_typeSummary == true)
                taskTypeAccummulator.addSummaryTable(document, summaryElement, "Task Type", setNames, personSets);
            // append state summary
            if (p_stateSummary == true)
                stateAccummulator.addSummaryTable(document, summaryElement, "State", setNames, personSets);
            Transformer transformer = TransformerFactory.newInstance().newTransformer();
            Source source = new DOMSource(document);
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            Result output = new StreamResult(outputStream);
            transformer.transform(source, output);
            xmlString = outputStream.toString();
            return xmlString;
        } catch(Exception e) {
            throw new EJBException(e);
        }
    }
    
    /**
     * Get a TimeEntry[] with all the details from a time search
     *
     * @param p_key User's security key
     * @param p_nodeLocal Node that the times are required for
     * @param p_contactableLocal[] The contactables whose times are required
     * @param p_recursive Should the sub-tree be searched?
     * @param p_start Start date for time
     * @param p_stop Stop date for time
     *
     * @ejb.interface-method view-type="local"
     **/
    public java.util.Map searchTimeLogValue(com.velocityme.interfaces.KeySessionLocal p_keyLocal,
                                            com.velocityme.interfaces.NodeLocal p_nodeLocal,
                                            com.velocityme.interfaces.ContactableLocal[] p_contactablesLocal,
                                            boolean p_recursive,
                                            boolean p_taskSummary,
                                            boolean p_typeSummary,
                                            boolean p_stateSummary,
                                            boolean p_monthSummary,
                                            boolean p_weekSummary,
                                            boolean p_daySummary,
                                            boolean p_entries,
                                            Date p_start,
                                            Date p_stop) {
        return searchTimeLogFilteredValue(p_keyLocal, p_nodeLocal, new ArrayList(), p_contactablesLocal, p_recursive, p_taskSummary,
                p_typeSummary, p_stateSummary, p_monthSummary, p_weekSummary, p_daySummary, p_entries, p_start, p_stop);
    }
    
    /**
     * Get a TimeEntry[] with all the details from a time search
     *
     * @param p_key User's security key
     * @param p_nodeLocal Node that the times are required for
     * @param p_contactableLocal[] The contactables whose times are required
     * @param p_recursive Should the sub-tree be searched?
     * @param p_start Start date for time
     * @param p_stop Stop date for time
     *
     * @ejb.interface-method view-type="local"
     **/
    public java.util.Map searchTimeLogFilteredValue(com.velocityme.interfaces.KeySessionLocal p_keyLocal,
                                            com.velocityme.interfaces.NodeLocal p_nodeLocal,
                                            java.util.Collection p_exclusionNodesLocal,
                                            com.velocityme.interfaces.ContactableLocal[] p_contactablesLocal,
                                            boolean p_recursive,
                                            boolean p_taskSummary,
                                            boolean p_typeSummary,
                                            boolean p_stateSummary,
                                            boolean p_monthSummary,
                                            boolean p_weekSummary,
                                            boolean p_daySummary,
                                            boolean p_entries,
                                            Date p_start,
                                            Date p_stop) {
        try {
            // set up the heading lists
            List personSets = new ArrayList();
            List setNames = new ArrayList();
            Set allPersons = new HashSet();
            for (int i = 0; i < p_contactablesLocal.length; i++) {
                setNames.add(p_contactablesLocal[i].getNodeLocal().getName());
                Set setContactablePersons = ContactableSessionUtil.getLocalHome().create().getNestedPersonSet(p_keyLocal, p_contactablesLocal[i]);
                personSets.add(setContactablePersons);
                allPersons.addAll(setContactablePersons);
            }
            // only add the total if there is more than one column of results
            if (setNames.size() > 1) {
                setNames.add("Total");
                personSets.add(allPersons);
            }
            String nodePathName = p_nodeLocal.getPathName();
            Pattern pattern = Pattern.compile(nodePathName);
            DateFormat monthFormatter = new SimpleDateFormat("yyyy-MM");
            DateFormat weekFormatter = new SimpleDateFormat("yyyy-'W'ww");
            DateFormat dayFormatter = new SimpleDateFormat("yyyy-MM-dd EE");
            SearchLocal searchLocal = m_searchSessionLocal.createSearchSpaceFilteredLocal(p_keyLocal, p_nodeLocal, p_exclusionNodesLocal, new Boolean(p_recursive));
            // create a search containing all the person nodes
            Collection personNodesLocal = new ArrayList();
            Iterator i = allPersons.iterator();
            while(i.hasNext()) {
                personNodesLocal.add(((PersonLocal)i.next()).getContactableLocal().getNodeLocal());
            }
            Map results = new HashMap();
            List timeEntries = new ArrayList();
            SearchLocal searchPersonLocal = SearchUtil.getLocalHome().create();
            SearchLevelLocal searchLevelPersonLocal = SearchLevelUtil.getLocalHome().create(searchPersonLocal, new Integer(0), personNodesLocal);
            Collection timesLocal = TimeUtil.getLocalHome().findTimeForPersons(searchLocal, searchPersonLocal, p_start, p_stop);
            m_searchSessionLocal.removeSearchSpaceLocal(p_keyLocal, searchLocal);
            m_searchSessionLocal.removeSearchSpaceLocal(p_keyLocal, searchPersonLocal);
            Iterator te = timesLocal.iterator();
            DurationAccummulator personAccummulator = new DurationAccummulator();
            DurationAccummulator taskAccummulator = new DurationAccummulator();
            DurationAccummulator taskTypeAccummulator = new DurationAccummulator();
            DurationAccummulator stateAccummulator = new DurationAccummulator();
            DurationAccummulator monthAccummulator = new DurationAccummulator();
            DurationAccummulator weekAccummulator = new DurationAccummulator();
            DurationAccummulator dayAccummulator = new DurationAccummulator();
            while(te.hasNext()) {
                TimeLocal teLocal = (TimeLocal)te.next();
                Long duration = teLocal.getDuration();
                PersonLocal personLocal = teLocal.getPersonLocal();
                Matcher matcher = pattern.matcher(teLocal.getTaskLocal().getNodeLocal().getPathName());
                String truncPathName = matcher.replaceFirst(".");
                // update the individual sums as required
                personAccummulator.add("Total", personLocal, duration);
                if (p_taskSummary == true)
                    taskAccummulator.add(truncPathName, personLocal, duration);
                if (p_typeSummary == true)
                    taskTypeAccummulator.add(new TaskStateMachineValueToString(teLocal.getTaskLocal().getTaskStateMachineLocal().getTaskStateMachineValue()), personLocal, duration);
                if (p_stateSummary == true)
                    stateAccummulator.add(new StateValueToString(teLocal.getStateLocal().getStateValue()), personLocal, duration);
                if (p_monthSummary == true)
                    monthAccummulator.add(monthFormatter.format(teLocal.getStartTime()), personLocal, duration);
                if (p_weekSummary == true)
                    weekAccummulator.add(weekFormatter.format(teLocal.getStartTime()), personLocal, duration);
                if (p_daySummary == true)
                    dayAccummulator.add(dayFormatter.format(teLocal.getStartTime()), personLocal, duration);
                // create the entries as required
                if (p_entries == true) {
                    timeEntries.add(new TimeEntry(teLocal.getPersonLocal().getContactableLocal().getNodeLocal().getName(),
                                                  truncPathName,
                                                  teLocal.getStateLocal().getNodeLocal().getName(),
                                                  teLocal.getStartTime(),
                                                  teLocal.getDuration(),
                                                  teLocal.getDescription()));
                }
            }
            if (p_entries == true) {
                // sort the time entries by start time
                Collections.sort(timeEntries, new Comparator() {
                    public int compare(Object o1, Object o2) {
                        return ((TimeEntry)o1).getDate().compareTo(((TimeEntry)o2).getDate());
                    }
                });
                results.put("Entries", timeEntries);
            }
            Collection names = new ArrayList();
            Iterator nt = setNames.iterator();
            while(nt.hasNext()) {
                names.add(nt.next().toString());
            }
            results.put("Names", names);
            Collection summaries = new ArrayList();
            summaries.add(personAccummulator.getSummaryTable("Person", personSets));
            if (p_taskSummary == true)
                summaries.add(taskAccummulator.getSummaryTable("Task", personSets));
            if (p_typeSummary == true)
                summaries.add(taskTypeAccummulator.getSummaryTable("Type", personSets));
            if (p_stateSummary == true)
                summaries.add(stateAccummulator.getSummaryTable("State", personSets));
            if (p_monthSummary == true)
                summaries.add(monthAccummulator.getSummaryTable("Month", personSets));
            if (p_weekSummary == true)
                summaries.add(weekAccummulator.getSummaryTable("Week", personSets));
            if (p_daySummary == true)
                summaries.add(dayAccummulator.getSummaryTable("Day", personSets));
            results.put("Summaries", summaries);
            return results;
        } catch(Exception e) {
            throw new EJBException(e);
        }
    }
    
    private java.util.Map calculateDurationRemaining(Collection p_tasksLocal,
                                                     Collection p_timesLocal,
                                                     boolean p_includeDuration,
                                                     boolean p_includeRemaining,
                                                     Date p_start,
                                                     Date p_stop,
                                                     boolean p_singleBin) {
        class TDDR {
            public TaskPK taskPK;
            public Date date;
            public Long duration_min;
            public Integer remaining_min;
            public TDDR(TaskPK taskPK, Date date, Long duration_min, Integer remaining_min) {
                this.taskPK = taskPK;
                this.date = date;
                this.duration_min = duration_min;
                this.remaining_min = remaining_min;
            }
        }
        SimpleDurationAccummulator accummulator = new SimpleDurationAccummulator(p_start, p_stop, p_singleBin);
        List entries = new ArrayList();
        if (p_includeRemaining) {
            Iterator te = p_tasksLocal.iterator();
            while (te.hasNext()) {
                try {
                    TaskLocal taskLocal = (TaskLocal)te.next();
                    Collection changeDeltasLocal = ChangeDeltaUtil.getLocalHome().findChangeDeltaByType(taskLocal.getNodeLocal().getDBObjectLocal(), m_changeDeltaTypeCreate);
                    Iterator cd = changeDeltasLocal.iterator();
                    Date date = null;
                    while (cd.hasNext()) {
                        ChangeDeltaLocal cdl = (ChangeDeltaLocal)cd.next();
                        date = cdl.getDate();
                    }
                    entries.add(new TDDR((TaskPK)taskLocal.getPrimaryKey(), date, 0L, taskLocal.getRemainingDuration_min(p_start)));
                } catch(Exception e) {
                    throw new EJBException(e);
                }
            }
        }
        Iterator te = p_timesLocal.iterator();
        while (te.hasNext()) {
            TimeLocal teLocal = (TimeLocal) te.next();
            Long duration = p_includeDuration ? teLocal.getDuration() : 0;
            TaskPK taskPK = (TaskPK) teLocal.getTaskLocal().getPrimaryKey();
            Integer remaining_min = p_includeRemaining ? teLocal.getRemainingDuration_min() : 0;
            entries.add(new TDDR(taskPK, teLocal.getStartTime(), duration, remaining_min));
        }
        te = entries.iterator();
        while (te.hasNext()) {
            TDDR tddr = (TDDR)te.next();
            if (tddr.date.before(p_stop)) // exclude times after stop
                if (tddr.date.before(p_start))
                    accummulator.add(p_start, tddr.duration_min, tddr.taskPK, tddr.remaining_min); // put all before start into first bin
                else
                    accummulator.add(tddr.date, tddr.duration_min, tddr.taskPK, tddr.remaining_min);
        }
        return accummulator.getSums();
    }
    
    /**
     * Get all the details task duration search
     *
     * @param p_key User's security key
     * @param p_nodeLocal Node that the times are required for
     *
     * @ejb.interface-method view-type="local"
     **/
    public java.util.Map searchDurationSum(com.velocityme.interfaces.KeySessionLocal p_keyLocal,
                                           com.velocityme.interfaces.NodeLocal p_nodeLocal,
                                           boolean p_includeDuration,
                                           boolean p_recursive,
                                           Date p_start,
                                           Date p_stop,
                                           boolean p_singleBin) {
        try {
            SearchLocal searchLocal = m_searchSessionLocal.createSearchSpaceLocal(p_keyLocal, p_nodeLocal, new Boolean(p_recursive));
            Collection timesLocal = TimeUtil.getLocalHome().findTimeByDate(searchLocal, p_start, p_stop);
            Collection tasksLocal = TaskUtil.getLocalHome().findTasks(searchLocal);
            m_searchSessionLocal.removeSearchSpaceLocal(p_keyLocal, searchLocal);
            return calculateDurationRemaining(tasksLocal, timesLocal, p_includeDuration, true, p_start, p_stop, p_singleBin);
        } catch(Exception e) {
            throw new EJBException(e);
        }
    }
    
    /**
     * Get all the details task duration search
     *
     * @param p_key User's security key
     * @param p_nodeLocal Node that the times are required for
     *
     * @ejb.interface-method view-type="local"
     **/
    public java.util.Map searchUnplannedSum(com.velocityme.interfaces.KeySessionLocal p_keyLocal,
                                            java.util.Collection p_personsLocal,
                                            boolean p_recursive,
                                            Date p_start,
                                            Date p_stop,
                                            boolean p_singleBin) {
        try {
            // create a search containing all the person nodes
            Collection personNodesLocal = new ArrayList();
            Iterator i = p_personsLocal.iterator();
            while(i.hasNext()) {
                personNodesLocal.add(((PersonLocal)i.next()).getContactableLocal().getNodeLocal());
            }
            SearchLocal searchPersonLocal = SearchUtil.getLocalHome().create();
            SearchLevelLocal searchLevelPersonLocal = SearchLevelUtil.getLocalHome().create(searchPersonLocal, new Integer(0), personNodesLocal);
            Collection timesLocal = TimeUtil.getLocalHome().findAllTimeForPersons(searchPersonLocal, p_start, p_stop);
            m_searchSessionLocal.removeSearchSpaceLocal(p_keyLocal, searchPersonLocal);
            return calculateDurationRemaining(new ArrayList(), timesLocal, true, false, p_start, p_stop, p_singleBin);
        } catch(Exception e) {
            throw new EJBException(e);
        }
    }
    
    /** Helper class which is used to sum times. It chooses a suitable formatter 
     * based on the date range.
     */
    private class SimpleDurationAccummulator {
    	private Map m_mapSum = new TreeMap();
    	private Map m_mapRemaining = new TreeMap();
    	private DateFormat m_formatter;
    	private class DateRemaining {
        	public Date date;
        	public int remaining_min;
        	public DateRemaining(Date date, int remaining_min) {
        		this.date = date;
        		this.remaining_min = remaining_min;
        	}
        }
    	
    	public SimpleDurationAccummulator(Date p_start, Date p_stop) {
    	    this(p_start, p_stop, false);
    	}
        
    	public SimpleDurationAccummulator(Date p_start, Date p_stop, boolean single) {
    		Date dateDiff = new Date(p_stop.getTime() - p_start.getTime());
    		int field = Calendar.DAY_OF_YEAR;
        	m_formatter = new SimpleDateFormat("yyyy-MM-dd EE");
        	if (dateDiff.getMonth() > 1) {
        		m_formatter = new SimpleDateFormat("yyyy-'W'ww");
        		field = Calendar.WEEK_OF_YEAR;
        	}
        	if (dateDiff.getMonth() > 6) {
        		m_formatter = new SimpleDateFormat("yyyy-MM");
        		field = Calendar.MONTH;	
        	}
        	if (dateDiff.getYear() - 70 > 3) {
        	    m_formatter = new SimpleDateFormat("yyyy");
        	    field = Calendar.YEAR;
        	}
        	if (single) {
        	    m_formatter = new SimpleDateFormat("-");
        	}
        	Calendar start = Calendar.getInstance();
        	start.setTime(p_start);
        	start.set(Calendar.SECOND, 0);
        	start.set(Calendar.MINUTE, 0);
        	start.set(Calendar.HOUR_OF_DAY, 0);
        	Calendar stop = Calendar.getInstance();
        	stop.setTime(p_stop);
        	for (; start.before(stop); start.add(field, 1)) {
            	String id = m_formatter.format(start.getTime());
            	m_mapSum.put(id, new Long(0));
            	m_mapRemaining.put(id, new HashMap());
            }
    	}
    	public void add(Date date, Long p_duration, TaskPK taskPK, Integer remaining_min) {
    		String id = m_formatter.format(date);
            //FIXME: there shouldn't be missing ids so why is this necessary!?
            if (!m_mapSum.containsKey(id)) {
                m_mapSum.put(id, new Long(0L));
                m_mapRemaining.put(id, new HashMap());
            }
            long sum = (Long)m_mapSum.get(id);
            sum += p_duration.longValue();
            m_mapSum.put(id, new Long(sum));
            // now do remaining
            Map remaining = (Map)m_mapRemaining.get(id);
            if (!remaining.containsKey(taskPK)) {
            	remaining.put(taskPK, new DateRemaining(date, remaining_min.intValue()));
            }
            else {
            	DateRemaining dr = (DateRemaining)remaining.get(taskPK);
            	if (dr.date.before(date))
            		dr.remaining_min = remaining_min.intValue();
            }
    	}
    	public Map getSums() {
    		Map integratedMap = new TreeMap();
    		Map integratedRemaining = new HashMap();
    		int durationSum_min = 0;
    		Iterator i = m_mapSum.keySet().iterator();
    		while (i.hasNext()) {
    			Object id = i.next();
                Long sum = (Long)m_mapSum.get(id);
                durationSum_min += sum.longValue() / 1000L / 60L;
    			// now do remaining
    			Map remaining = (Map)m_mapRemaining.get(id);
    			integratedRemaining.putAll(remaining);
    			Iterator j = integratedRemaining.values().iterator();
    			int remainingSum_min = 0;
    			while (j.hasNext()) {
    				DateRemaining dr = (DateRemaining)j.next();
    				remainingSum_min += dr.remaining_min; 
    			}
    			integratedMap.put(id, new DurationRemaining(durationSum_min, remainingSum_min));
    		}
    		return integratedMap;
    	}
    }
    
    /** Helper class which is used to sum durations for combinations of an id
     * and a personPK. The id could be for a Task or a State or a TaskStateMachine.
     * It is assumed that the id is comparable so that the results will come out
     * in some sort of order.
     */
    private class DurationAccummulator {
        private Map m_mapValuePerson = new TreeMap();
        public DurationAccummulator() {
        }
        public void add(Object p_id, Object p_person, Long p_duration) {
            if (!m_mapValuePerson.containsKey(p_id))
                m_mapValuePerson.put(p_id, new HashMap());
            Map mapPersonSum = (Map)m_mapValuePerson.get(p_id);
            if (!mapPersonSum.containsKey(p_person))
                mapPersonSum.put(p_person, p_duration);
            else {
                long sum = ((Long)mapPersonSum.get(p_person)).longValue();
                sum += p_duration.longValue();
                mapPersonSum.put(p_person, new Long(sum));
            }
        }
        private Iterator getIdIterator() {
            return m_mapValuePerson.keySet().iterator();
        }
        private List getSumsForId(Object p_id, List p_personSets) {
            List sums = new ArrayList();
            Map mapPersonSum = (Map)m_mapValuePerson.get(p_id);
            Iterator s = p_personSets.iterator();
            while(s.hasNext()) {
                Set persons = (Set)s.next();
                long sum = 0;
                Iterator p = persons.iterator();
                while(p.hasNext()) {
                    Long personSum = (Long)mapPersonSum.get(p.next());
                    if (personSum != null)
                        sum += personSum.longValue();
                }
                sums.add(new Long(sum));
            }
            return sums;
        }
        public void addSummaryTable(Document p_document, Element p_parentElement, String p_name, List p_setNames, List p_personSets) {
            Element stElement = p_document.createElement("SummaryTable");
            p_parentElement.appendChild(stElement);
            stElement.setAttribute("name", p_name);
            // put in the list of column names
            Iterator nt = p_setNames.iterator();
            while(nt.hasNext()) {
                Element nameElement = p_document.createElement("Name");
                stElement.appendChild(nameElement);
                nameElement.setAttribute("value", nt.next().toString());
            }
            // now add a row for each task
            Iterator ts = getIdIterator();
            while(ts.hasNext()) {
                Element valueElement = p_document.createElement("Value");
                stElement.appendChild(valueElement);
                Object id = ts.next();
                valueElement.setAttribute("name", id.toString());
                List sums = getSumsForId(id, p_personSets);
                Iterator pt = sums.iterator();
                while(pt.hasNext()) {
                    Element sumElement = p_document.createElement("Sum");
                    valueElement.appendChild(sumElement);
                    sumElement.setAttribute("value", DurationFormat.formatDuration(((Long)pt.next()).longValue()));
                }
            }
        }
        public TimeSummaryTable getSummaryTable(String p_name, List p_personSets) {
            TimeSummaryTable results = new TimeSummaryTable(p_name);
            // now add a row for each task
            Iterator ts = getIdIterator();
            while(ts.hasNext()) {
                Object id = ts.next();
                String name = id.toString();
                Collection sumElements = new ArrayList();
                List sums = getSumsForId(id, p_personSets);
                Long[] longArray = new Long[sums.size()];
                Iterator i = sums.iterator();
                int index = 0;
                while(i.hasNext()) {
                    longArray[index++] = (Long)i.next();
                }
                results.addRow(name, longArray);
            }
            return results;
        }
    }
    
    /**
     * If the user has permission to delete nodes then go ahead and delete it.
     *
     * @param p_key User's security key
     * @param p_nodeLocal The node
     * @return The parent node
     *
     * @ejb.interface-method view-type="local"
     **/
    public NodeLocal deleteNode(KeySessionLocal p_keyLocal,
                                NodeLocal p_nodeLocal)
        throws PermissionDeniedException, InvalidKeyException {
        try {
            // can't delete system nodes
            if (!p_nodeLocal.getFinalType().equals("SystemNode") &&
                m_searchSessionLocal.getAssignedPermissionsLocal(p_keyLocal, p_nodeLocal).contains(m_permissionNodeDelete)) {
                ChangeDeltaLocal changeDeltaLocal = ChangeDeltaUtil.getLocalHome().create(new Date(), ChangeDeltaTypeBean.DELETE_NODE, p_keyLocal.getUserLocal(), p_nodeLocal.getParentNodeLocal().getDBObjectLocal());
                changeDeltaLocal.addItem(ChangeDeltaItemTypeBean.DELETE_CHILD, p_nodeLocal.getName());
                p_nodeLocal.getDBObjectLocal().setIsDeleted(Boolean.TRUE);
                return p_nodeLocal.getParentNodeLocal();
            }
            else
                throw new PermissionDeniedException();
        }
        catch(NamingException e) {
            throw new EJBException(e);
        }
        catch(CreateException e) {
            throw new EJBException(e);
        }
    }
    
    /**
     * Get the history for a node
     *
     * @param p_key User's security key
     * @param p_node Node that we want a history for
     *
     * @ejb.interface-method view-type="local"
     **/
    public com.velocityme.utility.SearchEntry getHistoryValue(KeySessionLocal p_keyLocal,
                                                              NodeLocal p_nodeLocal)
        throws PermissionDeniedException, InvalidKeyException {
        try {
            if (m_searchSessionLocal.getAssignedPermissionsLocal(p_keyLocal, p_nodeLocal).contains(m_permissionNodeView)) {
                SearchEntry searchEntry = new SearchEntry(p_nodeLocal.getNodeId(), new Integer(0));
                Collection created = p_nodeLocal.getDBObjectLocal().ejbSelectChangeDeltaByType(p_nodeLocal.getDBObjectLocal(), m_cdHelper.getDeltaTypeLocal(ChangeDeltaTypeBean.CREATE));
                if (created.size() > 0) {
                    Iterator i = created.iterator();
                    ChangeDeltaLocal cdLocal = (ChangeDeltaLocal)i.next();
                    SearchEntry.Transaction transaction = searchEntry.addTransaction(cdLocal.getChangeDeltaTypeLocal().getName(), cdLocal);
                    transaction.addRow("Description", p_nodeLocal.getDescription());
                }
                // get a collection of all the change deltas
                Set acceptableItems = new HashSet();
                acceptableItems.add(new Integer(ChangeDeltaItemTypeBean.DESCRIPTION));
                acceptableItems.add(new Integer(ChangeDeltaItemTypeBean.STATE));
                acceptableItems.add(new Integer(ChangeDeltaItemTypeBean.STATUS));
                acceptableItems.add(new Integer(ChangeDeltaItemTypeBean.MESSAGE));
                acceptableItems.add(new Integer(ChangeDeltaItemTypeBean.RESPONSIBILITY));
                Collection changesLocal = p_nodeLocal.getDBObjectLocal().getChangeDeltasLocal();
                Iterator ce = changesLocal.iterator();
                while(ce.hasNext()) {
                    ChangeDeltaLocal cdLocal = (ChangeDeltaLocal)ce.next();
                    SearchEntry.Transaction transaction = null;
                    if (cdLocal.getChangeDeltaTypeLocal().getName().equals("Change State"))
                        transaction = searchEntry.addTransaction("Change", cdLocal);
                    if (cdLocal.getChangeDeltaTypeLocal().getName().equals("Post Comment"))
                        transaction = searchEntry.addTransaction("Comment", cdLocal);
                    if (cdLocal.getChangeDeltaTypeLocal().getName().equals("Reassign Responsibility"))
                        transaction = searchEntry.addTransaction("Reassign", cdLocal);
                    if (transaction != null) {
                        Iterator ci = cdLocal.getChangeDeltaItemsLocal().iterator();
                        while(ci.hasNext()) {
                            ChangeDeltaItemLocal cdItemLocal = (ChangeDeltaItemLocal)ci.next();
                            // only put acceptable change log items into the history
                            if (acceptableItems.contains(cdItemLocal.getChangeDeltaItemTypeLocal().getChangeDeltaItemTypeId()))
                                transaction.addRow(cdItemLocal.getChangeDeltaItemTypeLocal().getName(),
                                                   cdItemLocal.getDescription());
                        }
                    }
                }
                return searchEntry;
            }
            else
                throw new PermissionDeniedException();
        }
        catch(FinderException e) {
            throw new EJBException(e);
        }
    }
    
    /**
     * Create the Session Bean.
     * @throws CreateException 
     */
    public void ejbCreate() throws CreateException {
        m_cdHelper = ChangeDeltaHelper.getInstance();
        try {
            m_searchSessionLocal = SearchSessionUtil.getLocalHome().create();
            m_permissionNodeCreate = PermissionUtil.getLocalHome().findByPrimaryKey(new PermissionPK(new Integer(PermissionBean.NODE_CREATE)));
            m_permissionNodeEdit = PermissionUtil.getLocalHome().findByPrimaryKey(new PermissionPK(new Integer(PermissionBean.NODE_EDIT)));
            m_permissionNodeDelete = PermissionUtil.getLocalHome().findByPrimaryKey(new PermissionPK(new Integer(PermissionBean.NODE_DELETE)));
            m_permissionNodePostComment = PermissionUtil.getLocalHome().findByPrimaryKey(new PermissionPK(new Integer(PermissionBean.NODE_POST_COMMENT)));
            m_permissionNodeView = PermissionUtil.getLocalHome().findByPrimaryKey(new PermissionPK(new Integer(PermissionBean.NODE_VIEW)));
            m_changeDeltaTypeCreate = ChangeDeltaTypeUtil.getLocalHome().findByPrimaryKey(new ChangeDeltaTypePK(new Integer(ChangeDeltaTypeBean.CREATE)));
        }
        catch(NamingException e) {
            throw new CreateException(e.getMessage());
        }
        catch(FinderException e) {
            throw new CreateException(e.getMessage());
        }
    }

    public void ejbActivate() throws java.rmi.RemoteException {
    }
    
    public void ejbPassivate() throws java.rmi.RemoteException {
    }
    
    public void ejbRemove() throws java.rmi.RemoteException {
    }
    
    public void setSessionContext(javax.ejb.SessionContext sessionContext) throws java.rmi.RemoteException {
        m_context = sessionContext;
    }
    
}
